﻿using System;
using System.Xml;
using Notoric.Model.Elements;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// A generic reference to another <see cref="ModelElement"/> instance.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The principal reason for creating this class was to have a simple and generic way
    /// to create serializable collections of references to elements that are owned by different parts
    /// of the elements tree. Using an <see cref="Collections.ElementCollection{T}"/> directly
    /// on the referenced element would be a mistake, because the element collection serializes
    /// the complete element's XML not a reference. Therefore, when deserialized, we would create
    /// duplicate elements.
    /// </para>
    /// <para>
    /// The <see cref="ElementReference{T}"/> class holds a .NET reference to the element during
    /// runtime, but writes just its <see cref="ModelElement.ID"/> to the underlying XML document.
    /// Thus, when deserialized, this class restores the previous reference leaving just one copy
    /// of the original element.
    /// </para>
    /// <para>
    /// In order to diminish the risk of a crash caused by a cross-reference, the reference is not
    /// deserialized until the <see cref="Reference"/> property is accessed for the first time.
    /// </para>
    /// <para>
    /// The class also implements an explicit conversion operator to the type of the referenced element
    /// for an ease of use.
    /// </para>
    /// </remarks>
    /// <typeparam name="T">The type of the referenced <see cref="ModelElement"/></typeparam>
    public class ElementReference<T> : ModelElement
        where T : ModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="ElementReference{T}"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="reference">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The referenced element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="reference"/> is not a part of the same <see cref="Package"/>.
        /// </exception>
        public ElementReference(XmlPackagePart packagePart, ModelElement owner, T reference)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (reference == null)
                throw new ArgumentNullException("reference");
            if (packagePart.Package != reference.PackagePart.Package)
                throw new ArgumentException("The referenced element must be a part of the same package!");

            this.reference = reference;
            this.referenceIDAttribute.Value = reference.ID.ToString();
        }

        /// <summary>
        /// Creates a new <see cref="ElementReference{T}"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal ElementReference(XmlPackagePart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            referenceIDAttribute = element.Attributes[PackageVocabulary.IDRefAttribute];
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.ElementReferenceElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            referenceIDAttribute = document.CreateAttribute(PackageVocabulary.IDRefAttribute);

            element.Attributes.Append(referenceIDAttribute);

            return element;
        }

        #endregion

        #region Conversion Operators

        /// <summary>
        /// Converts a given element reference to the referenced element.
        /// </summary>
        /// <param name="reference">
        /// <para>
        /// Type: <see cref="ElementReference{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The converted reference.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The element referenced by <paramref name="reference"/>.
        /// </para>
        /// </returns>
        public static explicit operator T(ElementReference<T> reference)
        {
            return reference.Reference;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the referenced element.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The referenced element.
        /// </para>
        /// </value>
        public T Reference
        {
            get 
            {
                if (reference == null)
                {
                    var refId = new Guid(referenceIDAttribute.Value);

                    reference = PackagePart.Package.IDMap.FindObject<T>(refId);
                }

                return reference;
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the referenced element.
        /// </summary>
        private T reference;
        /// <summary>
        /// Holds the ID of the referenced element.
        /// </summary>
        private XmlAttribute referenceIDAttribute;

        #endregion
    }
}
